using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;

namespace MedievalGameProject
{
    // Represents different states of the game
    public enum GameState
    {
        SignIn, FindSession,
        CreateSession, Start, InGame, GameOver
    }

    // Represents different types of network messages
    public enum MessageType
    {
        StartGame, EndGame, RestartGame,
        RejoinLobby, UpdatePlayerPos, ShootArrow,
        StartNewRound
    }

    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        private static bool debugMode = false;
        private static bool loadInAutomatically = false;
        public static Game1 instance;
        public static ThirdPersonCamera camera;
        public static ActorManager actorManager;
        public static SoundManager soundManager;
        public static Terrain terrain;

        public Vector3 hostSpawnPoint = new Vector3(413, 74, 68);
        public Vector3 remoteSpawnPoint = new Vector3(-230, 74, 75);

        public static Scorekeeper scoreKeeper;
        public static int scoreToWin = 3;

        // Current game state
        public static GameState currentGameState = GameState.SignIn;

        // Network stuff
        static NetworkSession networkSession;
        static PacketWriter packetWriter = new PacketWriter();
        static PacketReader packetReader = new PacketReader();

        // fonts
        SpriteFont basicFont;
        SpriteFont titleFont;

        // sprites
        Texture2D reticleTexture;
        Texture2D startShadowTexture1;
        Texture2D startShadowTexture2;
        Texture2D endTexture;

        // start screen sprite y-origin positions
        float shadowPosition1;
        float shadowPosition2;

        public Game1()
        {
            instance = this;
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // enables all of the networking and gamer services
            Components.Add(new GamerServicesComponent(this));

            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
            graphics.ApplyChanges();

            actorManager = new ActorManager(this);
            soundManager = new SoundManager(this);

            terrain = new Terrain(this);
            terrain.Load("terrainheightmap", 256, 256, 5, 1);

            camera = new ThirdPersonCamera(this, new Vector3(0, 0, 0), null, Vector3.Up);

            scoreKeeper = new Scorekeeper();

            Components.Add(camera);
            Components.Add(soundManager);

            shadowPosition1 = Window.ClientBounds.Height;
            shadowPosition2 = Window.ClientBounds.Height;

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // Load fonts
            basicFont = Content.Load<SpriteFont>(@"fonts\basicFont");
            titleFont = Content.Load<SpriteFont>(@"fonts\titleFont");

            reticleTexture = Content.Load<Texture2D>(@"sprites\Reticle");
            startShadowTexture1 = Content.Load<Texture2D>(@"sprites\manShadow");
            startShadowTexture2 = Content.Load<Texture2D>(@"sprites\womanShadow");
            endTexture = Content.Load<Texture2D>(@"sprites\manWithArrows");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            
            // Only run the Update code if the game is currently active.
            // This prevents the game from progressing while gamer services windows are open.
            if (this.IsActive)
            {
                // Run different methods based on game state
                switch (currentGameState)
                {
                    case GameState.SignIn:
                        Update_SignIn();
                        break;
                    case GameState.FindSession:
                        Update_FindSession();
                        break;
                    case GameState.CreateSession:
                        Update_CreateSession();
                        break;
                    case GameState.Start:
                        Update_Start(gameTime);
                        break;
                    case GameState.InGame:
                        Update_InGame(gameTime);
                        break;
                    case GameState.GameOver:
                        Update_GameOver(gameTime);
                        break;
                }
            }

            // Update the network session and pump network messages
            if (networkSession != null)
                networkSession.Update();

            base.Update(gameTime);
        }

        protected void Update_SignIn()
        {
            // If no local gamers are signed in, show sign-in screen
            if (Gamer.SignedInGamers.Count < 1)
            {
                Guide.ShowSignIn(1, false);
            }
            else
            {
                // Local gamer signed in, move to find sessions
                currentGameState = GameState.FindSession;
            }
        }

        private void Update_FindSession()
        {
            // Find sesssions of the current game
            AvailableNetworkSessionCollection sessions = NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
            if (sessions.Count == 0)
            {
                // If no sessions exist, move to the CreateSession game state
                currentGameState = GameState.CreateSession;
            }
            else
            {
                // If a session does exist, join it, wire up events, and move to the Start game state
                networkSession = NetworkSession.Join(sessions[0]);
                WireUpEvents();
                currentGameState = GameState.Start;
            }
        }

        protected void WireUpEvents()
        {
            // Wire up events for gamers joining and leaving
            networkSession.GamerJoined += GamerJoined;
            networkSession.GamerLeft += GamerLeft;
        }

        void GamerJoined(object sender, GamerJoinedEventArgs e)
        {
            if(e.Gamer.IsLocal)
                e.Gamer.Tag = CreateLocalCharacter();
            else
                e.Gamer.Tag = CreateRemoteCharacter();

            scoreKeeper.AddPlayerToScoreBoard(e.Gamer.Gamertag);
        }

        private Character CreateLocalCharacter()
        {
            LocalNetworkGamer gamer = networkSession.LocalGamers[0];
            Vector3 spawnLocation, otherSpawnLocation;
            if (gamer.IsHost)
            {
                spawnLocation = hostSpawnPoint;
                otherSpawnLocation = remoteSpawnPoint;
                camera.cameraPosition = new Vector3(431, 75, 59);
            }
            else
            {
                spawnLocation = remoteSpawnPoint;
                otherSpawnLocation = hostSpawnPoint;
                camera.cameraPosition = new Vector3(-250, 74, 77);
            }

            Vector3 facingDirection = (otherSpawnLocation - spawnLocation);
            facingDirection.Normalize();

            BasicModel model = new BasicModel(this, Content.Load<Model>(@"Models\archer"), 0.15f);
            model.FlipFacing();
            Character character = new Character(this, model, spawnLocation, 10);
            character.SetFacingDirection2D(facingDirection);
            character.usesGravity = true;
            character.activeWeapon = new Bow(this, character);
            character.SetController(new PlayerController());
            character.isLocal = true;

            camera.SetTarget(character);

            actorManager.localPlayer = character;

            return character;
        }

        private Character CreateRemoteCharacter()
        {
            BasicModel model = new BasicModel(this, Content.Load<Model>(@"Models\archer"), 0.15f);
            model.FlipFacing();

            Character character2 = new Character(this, model, new Vector3(0, 0, 0), 10);
            character2.usesGravity = true;
            character2.activeWeapon = new Bow(this, character2);
            character2.isLocal = false;

            actorManager.remotePlayer = character2;

            return character2;
        }

        private void RespawnPlayers()
        {
            LocalNetworkGamer localGamer = networkSession.LocalGamers[0];

            ((Character)localGamer.Tag).Respawn();
            ((Character)GetOtherPlayer().Tag).Respawn();
        }

        void GamerLeft(object sender, GamerLeftEventArgs e)
        {
            // Dispose of the network session, set it to null; go back to searching for sessions.
            networkSession.Dispose();
            networkSession = null;

            currentGameState = GameState.FindSession;
        }

        private void Update_CreateSession()
        {
            // Create a new session using SystemLink with a max of 1 local player and a max of 2 total players
            networkSession = NetworkSession.Create(NetworkSessionType.SystemLink, 1, 2);
            networkSession.AllowHostMigration = true;
            networkSession.AllowJoinInProgress = false;

            // Wire up events and move to the Start game state
            WireUpEvents();
            currentGameState = GameState.Start;
        }

        private void Update_Start(GameTime gameTime)
        {
            // Get local gamer
            LocalNetworkGamer localGamer = networkSession.LocalGamers[0];

            // Check for game start key or button press only if there are two players
            if (networkSession.AllGamers.Count == 2)
            {
                // If space bar or Start button is pressed, begin the game
                if (Keyboard.GetState().IsKeyDown(Keys.Space) ||
                    (GamePad.GetState(PlayerIndex.One).Buttons.Start == ButtonState.Pressed))
                {
                    // Send message to other player that we're starting
                    packetWriter.Write((int)MessageType.StartGame);
                    localGamer.SendData(packetWriter, SendDataOptions.Reliable);

                    // Call StartGame
                    StartGame();
                }

                // to animate second archer image on start screen
                if (shadowPosition2 > 120)
                {
                    shadowPosition2 -= 3f;
                }
            }

            // to animate first archer image on start screen
            if (shadowPosition1 > 40)
            {
                shadowPosition1 -= 3f;
            }

            if (loadInAutomatically)
                StartGame();

            // Process any incoming packets; other player has pressed the space bar to start the game
            ProcessIncomingData(gameTime);
        }

        protected void StartGame()
        {
            // Set game state to InGame
            currentGameState = GameState.InGame;
        }

        /*
         * Read the MessageType enum value from the start of the incoming packet and
         * call the appropriate method to handle whatever type of message was received
         */
        protected void ProcessIncomingData(GameTime gameTime)
        {
            // Process incoming data
            LocalNetworkGamer localGamer = networkSession.LocalGamers[0];

            // While there are packets to be read...
            while (localGamer.IsDataAvailable)
            {
                // Get the packet
                NetworkGamer sender;
                localGamer.ReceiveData(packetReader, out sender);

                // Ignore the packet if you sent it
                if (!sender.IsLocal)
                {
                    // Read messagetype from start of packet and call appropriate method
                    MessageType messageType = (MessageType)packetReader.ReadInt32();
                    switch (messageType)
                    {
                        case MessageType.EndGame:
                            EndGame();
                            break;
                        case MessageType.StartGame:
                            StartGame();
                            break;
                        case MessageType.RejoinLobby:
                            RejoinLobby();
                            break;
                        case MessageType.RestartGame:
                            RestartGame();
                            break;
                        case MessageType.UpdatePlayerPos:
                            UpdateRemotePlayer(gameTime);
                            break;
                        case MessageType.ShootArrow:
                            SpawnNewArrow();
                            break;
                        case MessageType.StartNewRound:
                            StartNewRound();
                            break;
                    }
                }
            }
        }

        protected void EndGame()
        {
            // Move to the game-over state
            currentGameState = GameState.GameOver;
        }

        private void RejoinLobby()
        {
            currentGameState = GameState.Start;
        }

        private void RestartGame()
        {
            RespawnPlayers();
            scoreKeeper.Clear();
            StartGame();
        }

        protected void UpdateRemotePlayer(GameTime gameTime)
        {
            // Get the other (non-local) player
            NetworkGamer theOtherGuy = GetOtherPlayer();

            // get the actor representing the other player
            Character otherPlayer = ((Character)theOtherGuy.Tag);

            // Read in the new position of the other player
            Vector3 otherGuyPos = packetReader.ReadVector3();
            Vector3 otherGuyDir = packetReader.ReadVector3();

            // Set the position
            otherPlayer.SetPosition(otherGuyPos);
            otherPlayer.SetFacingDirection2D(otherGuyDir);
        }

        protected static NetworkGamer GetOtherPlayer()
        {
            // Search through the list of players and find the one that's remote
            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                if (!gamer.IsLocal)
                {
                    return gamer;
                }
            }

            return null;
        }

        protected void SpawnNewArrow()
        {
            Console.WriteLine("Spawning new arrow for enemy");

            // Get the other (non-local) player
            NetworkGamer theOtherGuy = GetOtherPlayer();

            // get the actor representing the other player
            Character otherPlayer = ((Character)theOtherGuy.Tag);

            Vector3 position = packetReader.ReadVector3();
            Vector3 direction = packetReader.ReadVector3();
            float speed = (float)packetReader.ReadDouble();

            otherPlayer.FireWeapon(position, direction, speed);
        }

        protected void StartNewRound()
        {
            actorManager.ScheduleRemoveAll();

            // Get local player
            LocalNetworkGamer localGamer = networkSession.LocalGamers[0];

            scoreKeeper.AddOneToPlayerScore(localGamer.Gamertag);

            if (scoreKeeper.ScoreGreaterThanOrEqual(scoreToWin))
                EndGame();
            else
            {
                RespawnPlayers();
                Console.WriteLine(scoreKeeper.ToString());
            }
        }

        private void Update_InGame(GameTime gameTime)
        {
            // Update the local player
            UpdateLocalPlayer(gameTime);

            actorManager.Update(gameTime);

            // Read any incoming data
            ProcessIncomingData(gameTime);

            // TODO: check for collisions and what not (it might be beneficial to have only one player, the host, check for all collisions)

            // TODO: check for end game condition
            //networkSession.LocalGamers[0].SendData(packetWriter, SendDataOptions.Reliable);
        }

        protected void UpdateLocalPlayer(GameTime gameTime)
        {
            // Get local player
            LocalNetworkGamer localGamer = networkSession.LocalGamers[0];

            // Get the local player's sprite
            Character localCharacter = (Character)localGamer.Tag;

            // Call the sprite's Update method, which will process user input for movement among other things
            localCharacter.Update(gameTime);

            // Send message to other player with message tag and new position of sprite
            packetWriter.Write((int)MessageType.UpdatePlayerPos);
            packetWriter.Write(localCharacter.position);
            packetWriter.Write(localCharacter.direction);

            // Send data to other player
            localGamer.SendData(packetWriter, SendDataOptions.InOrder);
        }

        public static void SendShootArrowMessage(Vector3 position, Vector3 direction, float speed)
        {
            Console.WriteLine("Sending shoot arrow message");

            // Send message to other player with message tag and new position of sprite
            packetWriter.Write((int)MessageType.ShootArrow);
            packetWriter.Write(position);
            packetWriter.Write(direction);
            packetWriter.Write((double)speed);

            // Get local player
            LocalNetworkGamer localGamer = networkSession.LocalGamers[0];

            // Send data to other player
            localGamer.SendData(packetWriter, SendDataOptions.InOrder);
        }

        public void SendStartNewRoundMessage()
        {
            actorManager.ScheduleRemoveAll();

            // Send message to other player with message tag and new position of sprite
            packetWriter.Write((int)MessageType.StartNewRound);

            // Get local player
            LocalNetworkGamer localGamer = networkSession.LocalGamers[0];

            // Send data to other player
            localGamer.SendData(packetWriter, SendDataOptions.InOrder);

            NetworkGamer otherGamer = GetOtherPlayer();
            scoreKeeper.AddOneToPlayerScore(otherGamer.Gamertag);

            if (scoreKeeper.ScoreGreaterThanOrEqual(scoreToWin))
                EndGame();
            else
                RespawnPlayers();
        }

        private void Update_GameOver(GameTime gameTime)
        {
            KeyboardState keyboardState = Keyboard.GetState();

            // If player presses Enter, restart game
            if (keyboardState.IsKeyDown(Keys.Enter))
            {
                // Send restart game message
                packetWriter.Write((int)MessageType.RestartGame);
                networkSession.LocalGamers[0].SendData(packetWriter, SendDataOptions.Reliable);

                RestartGame();
            }
            // If player presses Escape, rejoin lobby
            if (keyboardState.IsKeyDown(Keys.Escape))
            {
                // Send rejoin lobby message
                packetWriter.Write((int)MessageType.RejoinLobby);
                networkSession.LocalGamers[0].SendData(packetWriter, SendDataOptions.Reliable);

                RejoinLobby();
            }

            // Read any incoming messages
            ProcessIncomingData(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            // Only draw when game is active
            if (this.IsActive)
            {
                // Based on the current game state,
                // call the appropriate method
                switch (currentGameState)
                {
                    // SignIn, FindSession, and CreateSession do nothing but draw a blank screen b/c other
                    // gaming services activities are happening, and no drawing on the screen is needed
                    case GameState.SignIn:
                    case GameState.FindSession:
                    case GameState.CreateSession:
                        GraphicsDevice.Clear(Color.ForestGreen);
                        break;
                    case GameState.Start:
                        DrawStartScreen();
                        break;
                    case GameState.InGame:
                        DrawInGameScreen(gameTime);
                        break;
                    case GameState.GameOver:
                        DrawGameOverScreen();
                        break;
                }
            }

            base.Draw(gameTime);
        }
        
        private void DrawStartScreen()
        {
            // Clear screen
            GraphicsDevice.Clear(Color.ForestGreen);

            // Draw text for intro splash screen
            spriteBatch.Begin();

            Vector2 pos1 = new Vector2(-10, shadowPosition1);
            Vector2 pos2 = new Vector2(Window.ClientBounds.Width - startShadowTexture2.Width + 20, shadowPosition2);

            if (networkSession.AllGamers.Count == 1)
            {
                spriteBatch.Draw(startShadowTexture1, pos1, Color.White);

                string waitingText = "Waiting for opponent";
                spriteBatch.DrawString(basicFont, waitingText,
                    new Vector2((Window.ClientBounds.Width / 2) - (basicFont.MeasureString(waitingText).X / 2),
                    (Window.ClientBounds.Height / 2) + 60),
                    Color.White);

                string playerText1 = networkSession.AllGamers[0].Gamertag;
                spriteBatch.DrawString(basicFont, playerText1,
                    new Vector2(40, Window.ClientBounds.Height - 60),
                    Color.White);
            }
            else if (networkSession.AllGamers.Count == 2)
            {
                spriteBatch.Draw(startShadowTexture1, pos1, Color.White);
                spriteBatch.Draw(startShadowTexture2, pos2, Color.White);

                string instructionText = "Press Spacebar to begin";
                spriteBatch.DrawString(basicFont, instructionText,
                    new Vector2((Window.ClientBounds.Width / 2) - (basicFont.MeasureString(instructionText).X / 2),
                    (Window.ClientBounds.Height / 2) - (basicFont.MeasureString(instructionText).Y / 2) + 60),
                    Color.White);

                string playerText1 = networkSession.AllGamers[0].Gamertag;
                spriteBatch.DrawString(basicFont, playerText1,
                    new Vector2(40, Window.ClientBounds.Height - 60),
                    Color.White);

                string playerText2 = networkSession.AllGamers[1].Gamertag;
                spriteBatch.DrawString(basicFont, playerText2,
                                    new Vector2(Window.ClientBounds.Width - basicFont.MeasureString(playerText2).X - 20, Window.ClientBounds.Height - 60),
                                    Color.White);
            }

            // title
            string titleText = "Super Archer Fighter Turbo II";
            spriteBatch.DrawString(titleFont, titleText,
                    new Vector2((Window.ClientBounds.Width / 2) - (titleFont.MeasureString(titleText).X / 2), 20),
                    Color.Black);

            spriteBatch.End();
        }

        private void DrawInGameScreen(GameTime gameTime)
        {
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            // Clear device
            GraphicsDevice.Clear(Color.White);

            terrain.Draw(gameTime);

            actorManager.Draw(gameTime);

            // Loop through all gamers in session
            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                ((Character)gamer.Tag).Draw(gameTime);
            }

            // reticle
            spriteBatch.Begin();
            Vector2 pos = new Vector2((Window.ClientBounds.Width / 2) - reticleTexture.Width - 2, (Window.ClientBounds.Height / 2) - (reticleTexture.Height / 2));
            spriteBatch.Draw(reticleTexture, pos, Color.White);
            ((Character)networkSession.LocalGamers[0].Tag).DrawHealthBar(spriteBatch, gameTime);
            spriteBatch.End();
        }

        private void DrawGameOverScreen()
        {
            // Clear device
            GraphicsDevice.Clear(Color.DarkRed);

            spriteBatch.Begin();

            Vector2 pos1 = new Vector2(10, 70);
            spriteBatch.Draw(endTexture, pos1, Color.White);

            // Game over. Find the chased sprite and draw his score.
            string text1 = "Game Over\n";
            text1 += scoreKeeper.ToString();

            // Give players instructions from here
            string text2 = "Press ENTER to play again";
            text2 += "\nPress ESCAPE to exit to game lobby";

            spriteBatch.DrawString(basicFont, text1,
                new Vector2(Window.ClientBounds.Width - basicFont.MeasureString(text1).X - 150, 110),
                Color.WhiteSmoke);

            spriteBatch.DrawString(basicFont, text2,
                new Vector2(Window.ClientBounds.Width - basicFont.MeasureString(text2).X - 40,
                    Window.ClientBounds.Height - basicFont.MeasureString(text2).Y - 50),
                Color.WhiteSmoke);

            spriteBatch.End();
        }

        public static bool IsDebug()
        {
            return debugMode;
        }
    }
}
